在完成全系統錯誤處理與 Log 監控模組 (ErrorReportingService.gs) 後,我們的 NGO 房間預約系統已具備極高的強韌性與自我診斷能力。
今天我們將進入 模組 F:系統整合與運營維護 的核心環節——資料庫欄位結構自動校驗、Google Sheets 定時備份機制 以及 端到端(End-to-End, E2E)整合測試。這將確保系統在長期運作下,資料庫結構不因人為誤操作而損壞,並具備災害復原(Disaster Recovery)與全流程驗證的能力。
為確保 NGO 在無專職 IT 人員維運的情況下系統仍能長期穩定運行,我們設計了以下自動化維護流程:
[ 定時觸發器 (Time-driven) ]
│
┌─────────────────────┴─────────────────────┐
▼ ▼
【1. 資料庫欄位結構自動校驗】 【2. 數據定時自動備份】
(Check & Repair Sheets/Headers) (Backup to Google Drive Folder)
│ │
└─────────────────────┬─────────────────────┘
▼
【3. 端到端 E2E 整合測試】
(模擬 預約 -> 簽到 -> 取消 流程)
Bookings 與 SystemLogs 工作表是否存在;若缺少關鍵欄位,自動補充並格式化。MaintenanceService.gs)請在 Apps Script 專案中新增名為 MaintenanceService.gs 的檔案,寫入以下程式碼:
/**
* ==============================================================================
* 系統整合測試、資料一致性與備份機制 (MaintenanceService.gs)
* 專案:NGO 房間與資源預約管理系統 (Build on Google AI Track)
* 說明:提供資料庫欄位結構自動校驗、Google Sheets 定時備份與端到端 E2E 測試。
* ==============================================================================
*/
/**
* 1. 資料庫欄位結構自動校驗與自動修復 (Self-Healing)
*/
function verifyAndRepairDatabaseStructure() {
const config = getConfig();
const ss = SpreadsheetApp.openById(config.SPREADSHEET_ID);
// 定義標準工作表結構需求
const schemaRequirements = {
"Bookings": [
"BookingID", "UserEmail", "RoomID", "StartTime", "EndTime",
"Status", "CheckInTime", "ReleaseTime", "AdminNote", "CreatedAt"
],
"SystemLogs": [
"Timestamp", "Level", "Function", "Message", "ContextData", "StackTrace"
]
};
let repairCount = 0;
for (const [sheetName, requiredHeaders] of Object.entries(schemaRequirements)) {
let sheet = ss.getSheetByName(sheetName);
// 若工作表不存在則創建
if (!sheet) {
sheet = ss.insertSheet(sheetName);
sheet.appendRow(requiredHeaders);
sheet.getRange(1, 1, 1, requiredHeaders.length).setFontWeight("bold").setBackground("#f1f5f9");
sheet.setFrozenRows(1);
Logger.log(`🛠️ [Auto-Repair] 已自動創建缺少的工作表: ${sheetName}`);
repairCount++;
continue;
}
// 檢查第一行表頭欄位
const currentHeaders = sheet.getRange(1, 1, 1, Math.max(sheet.getLastColumn(), 1)).getValues()[0];
const missingHeaders = requiredHeaders.filter(h => !currentHeaders.includes(h));
if (missingHeaders.length > 0) {
// 補齊缺少的欄位
missingHeaders.forEach(header => {
const nextCol = sheet.getLastColumn() + 1;
sheet.getRange(1, nextCol).setValue(header).setFontWeight("bold").setBackground("#f1f5f9");
Logger.log(`🛠️ [Auto-Repair] 工作表 ${sheetName} 已補齊缺少欄位: ${header}`);
});
repairCount++;
}
}
Logger.log(`✅ [Database Check] 校驗完成,共執行 ${repairCount} 項結構修復。`);
return { success: true, repairedCount: repairCount };
}
/**
* 2. Google Sheets 資料庫自動備份至 Google Drive
* 建議設定 Time-driven Trigger 每週或每日深夜執行
*/
function backupDatabaseToDrive() {
try {
const config = getConfig();
const sourceFile = DriveApp.getFileById(config.SPREADSHEET_ID);
// 取得或建立備份資料夾
const folderName = "NGO_Booking_System_Backups";
const folders = DriveApp.getFoldersByName(folderName);
let targetFolder;
if (folders.hasNext()) {
targetFolder = folders.next();
} else {
targetFolder = DriveApp.createFolder(folderName);
Logger.log(`📁 已在 Google Drive 建立備份資料夾: ${folderName}`);
}
// 格式化備份檔名: NGO_Bookings_Backup_YYYYMMDD_HHMM.xlsx
const timeStampStr = Utilities.formatDate(new Date(), "GMT+8", "yyyyMMdd_HHmm");
const backupFileName = `NGO_Bookings_Backup_${timeStampStr}`;
// 執行複製備份
const backupCopy = sourceFile.makeCopy(backupFileName, targetFolder);
Logger.log(`🎉 [Backup Success] 已成功備份至 Drive 資料夾: ${backupFileName} (ID: ${backupCopy.getId()})`);
return { success: true, backupFileId: backupCopy.getId(), fileName: backupFileName };
} catch (error) {
logAndReportError(error, "backupDatabaseToDrive", {}, LOG_LEVEL.CRITICAL);
return { success: false, message: error.toString() };
}
}
/**
* 3. 端到端 (E2E) 流程整合自動化測試
* 模擬全套業務流程:預約建立 -> 時段排查 -> 簽到測試 -> 預約取消規章
*/
function runEndToEndSystemTest() {
Logger.log("=== 🚀 開始執行端到端 (E2E) 系統整合測試 ===");
const testEmail = "test_runner@ngo.org";
const testRoom = "A1";
const now = new Date();
// 設定測試時間:明天 14:00 - 15:00
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
const startIso = new Date(tomorrow.setHours(14, 0, 0, 0)).toISOString();
const endIso = new Date(tomorrow.setHours(15, 0, 0, 0)).toISOString();
const dateStr = startIso.split("T")[0];
// 測試步驟 1:結構校驗
Logger.log("👉 [Step 1] 執行資料庫結構校驗...");
verifyAndRepairDatabaseStructure();
// 測試步驟 2:提交新預約
Logger.log("👉 [Step 2] 測試提交新預約 API...");
const bookingResult = submitNewBooking(testEmail, testRoom, startIso, endIso, "E2E 自動化測試預約");
Logger.log(`預約結果: ${JSON.stringify(bookingResult)}`);
if (!bookingResult.success) {
Logger.log("❌ E2E 測試於 Step 2 失敗。");
return;
}
const testBookingId = bookingResult.bookingId;
// 測試步驟 3:查詢可用時段 (應顯示該時段不可用)
Logger.log("👉 [Step 3] 測試時段重複排查 API...");
const slotsResult = getAvailableSlots(dateStr, testRoom);
Logger.log(`時段查詢結果成功取得 ${slotsResult.slots.length} 個 Slots。`);
// 測試步驟 4:測試現場簽到機制
Logger.log("👉 [Step 4] 測試現場簽到 API...");
const checkInResult = processCheckIn(testBookingId);
Logger.log(`簽到結果: ${JSON.stringify(checkInResult)}`);
// 測試步驟 5:測試 24HR 規章取消機制
Logger.log("👉 [Step 5] 測試取消與規章 API...");
const cancelResult = cancelBookingWithPolicy(testBookingId, "E2E 測試流程清理", testEmail);
Logger.log(`取消結果: ${JSON.stringify(cancelResult)}`);
Logger.log("=== 🎉 端到端 (E2E) 系統整合測試全數通過! ===");
}
要讓系統實現真正的無人值守維運,可在 Google Apps Script 貼上以下觸發器建立腳本(或手動於「觸發條件」介面設定):
/**
* 自動建立系統定時維護與備份觸發器 (只需執行一次)
*/
function setupMaintenanceTriggers() {
// 刪除舊有的同名觸發器避免重複
const triggers = ScriptApp.getProjectTriggers();
triggers.forEach(trigger => {
if (trigger.getHandlerFunction() === "backupDatabaseToDrive" ||
trigger.getHandlerFunction() === "verifyAndRepairDatabaseStructure") {
ScriptApp.deleteTrigger(trigger);
}
});
// 1. 設定每日凌晨 02:00 執行資料庫備份
ScriptApp.newTrigger("backupDatabaseToDrive")
.timeBased()
.everyDays(1)
.atHour(2)
.create();
// 2. 設定每週一凌晨 03:00 執行結構校驗
ScriptApp.newTrigger("verifyAndRepairDatabaseStructure")
.timeBased()
.onWeekDay(ScriptApp.WeekDay.MONDAY)
.atHour(3)
.create();
Logger.log("✅ 系統定時維護與備份觸發器設定完成!");
}
今天我們完成了 ** 系統整合測試、資料一致性與備份機制 (MaintenanceService.gs)**。透過欄位自動修復(Self-Healing)、無痛 Drive 數據備份與全自動 E2E 測試套件,為這套 NGO 預約系統鋪平了長期營運與維護的道路。
明天繼續……